[ 源码分享 ] Test317-用CrankSlider优雅地画弧线
今天在 twitter 上闲逛,刷到了 kynd 早期做的一个有关 crank slide 的 Gif,顿时被这个结构迷住了。于是尝试用 C++ 实现一下。这里分享下源码。
(Kynd 的 Gif 原图)
要实现这一效果需要哪些基础知识?只需要了解简单的三角函数(用代码画画-详解三角函数)与基础的向量知识即可。
观察可以发现。有两个点在做圆周运动(这一部分可使用三角函数)。其中一条直线连接了两个点,并且长度固定。直线的左端点始终连接着大圆上的点,而小圆上的点是用作确定朝向的,使用向量知识,根据定点,方向,长度,就可以计算出直线右端点的位置。最后再把直线右端点的轨迹记录,并绘制出来就能还原以上效果。
梳理完思路,就可以开始写代码。但简单的 copy 没有意思。我希望在实现基础效果的同时,延伸一些组合变化。例如将大量的 “Crank Slide” 组装,会形成怎样的轨迹?
下面是一些试验效果
Openframeworks 源码
以上效果都可以通过下述代码实现。需要创建两个 class。一个用于绘制圆,一个用于绘制直线
WenzyCircle 类
class WenzyCircle{
public:
float angle;
float speed;
float basicR;
ofVec2f centerPos;
ofVec2f pos;
WenzyCircle(){
}
WenzyCircle(ofVec2f centerPos_,float basicR_,float startAngle_,float speed_){
centerPos = centerPos_;
angle = startAngle_;
basicR = basicR_;
speed = speed_;
}
void update(){
angle += speed;
pos.x = centerPos.x + cos(angle) * basicR;
pos.y = centerPos.y + sin(angle) * basicR;
}
void drawBackCircle(){
// 底圆
ofNoFill();
ofDrawCircle(centerPos,basicR);
}
void drawFrontCircle(float r){
ofFill();
ofDrawCircle(pos,r);
}
};
WenzyConnectLine 类
#include "WenzyCircle.h"
class WenzyConnectLine{
public:
float lineL; // 长度
vector<ofVec2f> posList; // 记录轨迹顶点
ofPolyline myPath;
ofVec2f startPos,pathPos;
WenzyConnectLine(){
}
WenzyConnectLine(float lineL_){
lineL = lineL_;
}
void update(WenzyCircle &circleA,WenzyCircle &circleB){
ofVec2f dir;
dir = circleB.pos - circleA.pos;
dir.normalize();
startPos = circleA.pos;
pathPos = circleA.pos + dir * lineL;
if(posList.size() < 700){
posList.push_back(pathPos);
}else{
posList.erase(posList.begin());
posList.push_back(pathPos);
}
}
void drawLine(){
ofDrawLine(startPos,pathPos);
}
void drawPath(){
myPath.clear();
for(int i = 0;i < posList.size();i++){
myPath.curveTo(posList[i]);
}
myPath.draw();
}
void drawPathCircle(float r){
ofFill();
ofDrawCircle(pathPos,r);
}
};
主程序
— ofApp.h 内
#include "WenzyCircle.h"
#include "WenzyConnectLine.h"
...
...
WenzyCircle circleA;
vector<WenzyCircle> sideCircles;
vector<WenzyConnectLine> connectlines;
ofEasyCam cam;
bool startMoving;
ofColor pathColor,circleColor;
— ofApp.cpp 内
void ofApp::setup(){
// 参数设置
circleA = WenzyCircle(ofVec2f(0,0), 200, 0, 0.015);
int circleNum = 12;
for(int i = 0;i < circleNum;i++){
float d = 200;
float angle = i/(float)circleNum * 2 * PI;
float x = cos(angle) * d;
float y = sin(angle) * d;
sideCircles.push_back(WenzyCircle(ofVec2f(x,y),50,0,0.015 * 5));
connectlines.push_back(WenzyConnectLine(300));
}
// 更新确定端点位置
circleA.update();
for(int i = 0;i < sideCircles.size();i++){
sideCircles[i].update();
connectlines[i].update(circleA, sideCircles[i]);
}
startMoving = false;
pathColor.set(240,203,112);
circleColor.set(255,143);
}
void ofApp::update(){
if(startMoving){
circleA.update();
for(int i = 0;i < sideCircles.size();i++){
sideCircles[i].update();
connectlines[i].update(circleA, sideCircles[i]);
}
}
}
void ofApp::draw(){
ofSetCircleResolution(50);
ofBackground(0);
// 绘制背景网格线
int girdW = 25;
ofSetColor(255,30);
for(int x = 0;x < ofGetWidth();x += girdW){
ofDrawLine(x,0,x,ofGetHeight());
}
for(int y = 0;y < ofGetHeight();y += girdW){
ofDrawLine(0,y,ofGetWidth(),y);
}
cam.begin();
ofSetColor(circleColor);
ofSetLineWidth(2);
circleA.drawBackCircle();
ofSetColor(255);
circleA.drawFrontCircle(5);
for(int i = 0;i < sideCircles.size();i++){
ofEnableBlendMode(OF_BLENDMODE_ALPHA);
ofSetColor(circleColor);
ofSetLineWidth(2);
sideCircles[i].drawBackCircle();
ofSetColor(255,230);
sideCircles[i].drawFrontCircle(5);
ofEnableBlendMode(OF_BLENDMODE_ADD);
ofSetLineWidth(4);
ofSetColor(255,ofRandom(200));
connectlines[i].drawLine();
ofSetLineWidth(2);
ofSetColor(pathColor,150);
connectlines[i].drawPath();
ofSetColor(pathColor);
connectlines[i].drawPathCircle(5);
}
cam.end();
}
void ofApp::keyPressed(int key){
if(key == 'r'){
startMoving = true;
}
}
代码浅析:
运行程序后,按 r 键开始绘制
整个代码非常短,不足 300 行。灵活组合可以有很强的延展性。通过修改 setup 函数内的“参数设置”代码,可以控制小圆的数量,以及大圆小圆的半径,位置以及顶点的起始角度,速度。不同参数产生不同效果
一点延展
上例中采用的链接方式是一个“大圆”带动多个“小圆”。也可以尝试另一种“联动”的组合。例如大圆带小圆 A,生成的端点再去带动小圆 B,如此循环,就像交接接力棒。
(另一种链接逻辑)
for(int i = 0;i < sideCircles.size();i++){
sideCircles[i].update();
if(i == 0){
connectlines[i].update(circleA, sideCircles[i]);
}else{
connectlines[i].update(connectlines[i - 1].pathPos, sideCircles[i]);
}
}
最后再加入一点“火光”去强化绘制的轨迹,用力去影响火花的速度,可以更好地体现端点变化的韵律感。
一些视频测试合集
https://v.qq.com/txp/iframe/player.html?vid=n0532y0k0ir&width=500&height=375&auto=0
End
Crank-slider mechanism 中文似乎称作“曲柄滑块机构”。 它是一种机械结构,只要有材料,完全可以在现实中制造一个互动装置。下图是在 twitter 中找到的一个动图,非常直观,来源作者 twitter@mechanisms。
如果你从旋转轴的角度望去,剖面的效果其实就有点接近下图了
区别是“红柱”的端点部分没有圆周运动,而且“蓝柱”与“金柱”的衔接处并不是直角。但这种非直角的处理,让整个联动在前后方向多了一丝变化,结果显然更有趣